package com.rizzo.shell.intern;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.rizzo.infrastructure.dark.redis.RedisDarkLauncher;
import com.rizzo.infrastructure.domain.KeyType;
import com.rizzo.infrastructure.redis.key.RedisKey;
import com.rizzo.infrastructure.redis.key.RedisKeyBuilder;
import com.rizzo.infrastructure.util.MatchingUtil;
import com.rizzo.shell.intern.args.MysqlTransferArguments;
import com.rizzo.shell.intern.args.RedisTransferArguments;
import com.rizzo.shell.intern.host.MysqlHost;
import com.rizzo.shell.intern.host.RedisHost;
import com.rizzo.shell.plugins.MysqlConfig;
import com.rizzo.shell.plugins.RedisConfig;
import org.joda.time.DateTime;
import org.joda.time.Period;
import org.ocpsoft.prettytime.PrettyTime;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

@Component
public class RedisCommands extends RizzoCommands implements CommandMarker {

    public static final String NEVER_EXPIRATION_MESSAGE = "never";

    public static final String NODE_TITLE_HEADER = "NODE";
    public static final String HOST_TITLE_HEADER = "HOST";
    public static final String PORT_TITLE_HEADER = "PORT";
    public static final String TYPE_TITLE_HEADER = "TYPE";
    public static final String KIND_TITLE_HEADER = "KIND";
    public static final String EXPIRATION_TITLE_HEADER = "EXPIRATION";
    public static final String DATA_TITLE_HEADER = "DATA";
    public static final String CATEGORY_TITLE_HEADER = "CATEGORY";
    public static final String USER_TITLE_HEADER = "USER";
    public static final String LAUNCHED_TITLE_HEADER = "LAUNCHED";

    private static Logger logger = Logger.getLogger("com.rizzo.shell.intern.RedisCommands");

    private final Set<RedisHost> redisHosts = ImmutableSet.of(
            new RedisHost(1, "localhost", "6379", ""),
            new RedisHost(2, "multivac", "6379", ""));

    private final Set<MysqlHost> mysqlHosts = ImmutableSet.of(
            new MysqlHost(1, "localhost", "9292", "sa", "", "rizzo_account", true),
            new MysqlHost(2, "localhost", "3306", "rizzo", "RizzO", "rizzo_account", false));

    @CliCommand(value = "redis:nodes", help = "Shows the current available Redis-nodes")
    public String nodes() throws Exception {
        logger.info("Listing possible Redis nodes...\n");
        logger.info(tabValues(WIDE_TAB_PADDING, NODE_TITLE_HEADER, HOST_TITLE_HEADER, PORT_TITLE_HEADER));
        for (RedisHost redisHost : redisHosts) {
            logger.info(redisHost.toString());
        }
        return "Listing done!\n";
    }

    @CliCommand(value = "redis:info", help = "Shows the structural content of a Redis-node")
    public String info(
            @CliOption(key = {"node"}, mandatory = false, help = "The node to ask the structural info from",
                    unspecifiedDefaultValue = "1") final int node,
            @CliOption(key = {"application"}, mandatory = false, help = "The application to ask the structural info from (begins with)",
                    unspecifiedDefaultValue = "*") final String application,
            @CliOption(key = {"type"}, mandatory = false, help = "The type of the keys to display (containing, case-insensitive)",
                    unspecifiedDefaultValue = "ALL") final KeyType type,
            @CliOption(key = {"category"}, mandatory = false, help = "The category of the keys to display (containing, case-insensitive)",
                    unspecifiedDefaultValue = "*") final String category,
            @CliOption(key = {"data"}, mandatory = false, help = "The data of the keys to display (containing, case-insensitive)",
                    unspecifiedDefaultValue = "*") final String data) throws Exception {
        logger.info("Printing info for node: " + node + "\n");
        RedisHost redisHost = getRedisHost(node);
        try {
            Map<RedisTransferArguments, String> args = ImmutableMap.<RedisTransferArguments, String>builder()
                    .put(RedisTransferArguments.HOST, redisHost.getHost())
                    .put(RedisTransferArguments.PORT, redisHost.getPort())
                    .put(RedisTransferArguments.PASSWORD, redisHost.getPassword()).build();
            AnnotationConfigApplicationContext componentContext = getApplicationContext("redisArgs", args, RedisConfig.class);

            RedisTemplate<RedisKey, String> redisKeyTemplate = (RedisTemplate) componentContext.getBean("redisKeyTemplate");
            RedisTemplate<String, String> stringRedisTemplate = (RedisTemplate) componentContext.getBean("stringRedisTemplate");

            Set<String> allApplicationKeys = stringRedisTemplate.keys(application + RedisKey.REDIS_WILDCARD);
            Set<String> sortedKeys = new TreeSet<String>(allApplicationKeys);
            String currentApplication = "";
            Integer foundKeys = 0;
            boolean first = true;
            for (String applicationKey : sortedKeys) {
                final RedisKey redisKey = RedisKey.deSerialize(applicationKey);
                if (!currentApplication.equals(redisKey.getApplication())) {
                    if (!first) {
                        logger.warning("-----------------------------------------------");
                        logger.warning("");
                    }
                    logger.log(Level.SEVERE, "----------- APPLICATION: (" + redisKey.getApplication() + ") -----------");
                    logger.warning(tabValues(MEDIUM_TAB_PADDING, TYPE_TITLE_HEADER, KIND_TITLE_HEADER,
                            EXPIRATION_TITLE_HEADER, DATA_TITLE_HEADER, CATEGORY_TITLE_HEADER));
                }
                currentApplication = redisKey.getApplication();
                String ttl = getTtl(redisKeyTemplate, redisKey);
                DataType dataType = redisKeyTemplate.type(redisKey);

                boolean matchesType = (type == KeyType.ALL | redisKey.getType() == type);
                boolean matchesData = data.equals(RedisKey.REDIS_WILDCARD)
                        || (StringUtils.hasText(redisKey.getData()) && redisKey.getData().toLowerCase().contains(data.toLowerCase()));
                boolean matchesCategory = category.equals(RedisKey.REDIS_WILDCARD)
                        || (StringUtils.hasText(redisKey.getCategory()) && redisKey.getCategory().toLowerCase().contains(category.toLowerCase()));
                if (matchesType && matchesData && matchesCategory) {
                    foundKeys++;
                    logger.info(tabValues(MEDIUM_TAB_PADDING, redisKey.getType().name(), dataType.name(), ttl, redisKey.getData(), redisKey.getCategory()));
                }
                first = false;
            }
            logger.info("Keys found: " + foundKeys);
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Fail!", e);
        }
        logger.info(DUMMY_SPACE);
        return "Info printing done!\n";
    }

    @CliCommand(value = "redis:expire", help = "Sets the expiration of keys")
    public String expire(
            @CliOption(key = {"node"}, mandatory = false, help = "The node to connect to",
                    unspecifiedDefaultValue = "1") final int node,
            @CliOption(key = {"application"}, mandatory = false, help = "The application of which the keys belong (begins with)",
                    unspecifiedDefaultValue = "*") final String application,
            @CliOption(key = {"category"}, mandatory = false, help = "The category of the keys to set the expiration time on (containing, case-insensitive)",
                    unspecifiedDefaultValue = "*") final String category,
            @CliOption(key = {"type"}, mandatory = false, help = "The type of the keys to set the expiration time",
                    unspecifiedDefaultValue = "ALL") final KeyType type,
            @CliOption(key = {"data"}, mandatory = false, help = "The data of the keys to set the expiration time on  (containing, case-insensitive)",
                    unspecifiedDefaultValue = "*") final String data,
            @CliOption(key = {"expiration"}, mandatory = true, help = "The expiration amount") final Long expiration,
            @CliOption(key = {"timeUnit"}, mandatory = true, help = "The unit of time") final TimeUnit timeUnit) throws Exception {
        logger.info("Setting expirations...\n");
        RedisHost redisHost = getRedisHost(node);
        Map<RedisTransferArguments, String> args = ImmutableMap.<RedisTransferArguments, String>builder()
                .put(RedisTransferArguments.HOST, redisHost.getHost())
                .put(RedisTransferArguments.PORT, redisHost.getPort())
                .put(RedisTransferArguments.PASSWORD, redisHost.getPassword()).build();
        this.findAndDo("redisArgs", args, application, category, type, data, new RedisFoundCallback() {
            @Override
            public void doInRedis(RedisKey redisKey, RedisTemplate<RedisKey, String> redisKeyTemplate) {
                logger.info("Setting expiration of key: " + redisKey.serialize() + " to " + expiration + " " + timeUnit.name());
                redisKeyTemplate.expire(redisKey, expiration, timeUnit);
            }
        });
        return "Expiration set!\n";
    }

    @CliCommand(value = "redis:del", help = "Deletes keys")
    public String delete(
            @CliOption(key = {"node"}, mandatory = false, help = "The node to connect to",
                    unspecifiedDefaultValue = "1") final int node,
            @CliOption(key = {"application"}, mandatory = false, help = "The application of which the keys belong (begins with)",
                    unspecifiedDefaultValue = "*") final String application,
            @CliOption(key = {"category"}, mandatory = false, help = "The category of the keys to delete (containing, case-insensitive)",
                    unspecifiedDefaultValue = "*") final String category,
            @CliOption(key = {"type"}, mandatory = false, help = "The type of the keys to delete",
                    unspecifiedDefaultValue = "ALL") final KeyType type,
            @CliOption(key = {"data"}, mandatory = false, help = "The data of the keys to delete (containing, case-insensitive)",
                    unspecifiedDefaultValue = "*") final String data) throws Exception {
        logger.info("Deleting keys...\n");
        RedisHost redisHost = getRedisHost(node);
        Map<RedisTransferArguments, String> args = ImmutableMap.<RedisTransferArguments, String>builder()
                .put(RedisTransferArguments.HOST, redisHost.getHost())
                .put(RedisTransferArguments.PORT, redisHost.getPort())
                .put(RedisTransferArguments.PASSWORD, redisHost.getPassword()).build();
        this.findAndDo("redisArgs", args, application, category, type, data, new RedisFoundCallback() {
            @Override
            public void doInRedis(RedisKey redisKey, RedisTemplate<RedisKey, String> redisKeyTemplate) {
                logger.info("Deleting key: " + redisKey.serialize());
                redisKeyTemplate.delete(redisKey);
            }
        });
        return "Deletion complete!\n";
    }

    @CliCommand(value = "redis:persist", help = "Persists keys (i.e. expiration to never)")
    public String persist(
            @CliOption(key = {"node"}, mandatory = false, help = "The node to connect to",
                    unspecifiedDefaultValue = "1") final int node,
            @CliOption(key = {"application"}, mandatory = false, help = "The application of which the keys belong (begins with)",
                    unspecifiedDefaultValue = "*") final String application,
            @CliOption(key = {"category"}, mandatory = false, help = "The category of the keys to persist (containing, case-insensitive)",
                    unspecifiedDefaultValue = "*") final String category,
            @CliOption(key = {"type"}, mandatory = false, help = "The type of the keys to persist",
                    unspecifiedDefaultValue = "ALL") final KeyType type,
            @CliOption(key = {"data"}, mandatory = false, help = "The data of the keys to persist (containing, case-insensitive)",
                    unspecifiedDefaultValue = "*") final String data) throws Exception {
        logger.info("Persisting keys...\n");
        RedisHost redisHost = getRedisHost(node);
        Map<RedisTransferArguments, String> args = ImmutableMap.<RedisTransferArguments, String>builder()
                .put(RedisTransferArguments.HOST, redisHost.getHost())
                .put(RedisTransferArguments.PORT, redisHost.getPort())
                .put(RedisTransferArguments.PASSWORD, redisHost.getPassword()).build();
        this.findAndDo("redisArgs", args, application, category, type, data, new RedisFoundCallback() {
            @Override
            public void doInRedis(RedisKey redisKey, RedisTemplate<RedisKey, String> redisKeyTemplate) {
                logger.info("Persisting key: " + redisKey.serialize());
                redisKeyTemplate.persist(redisKey);
            }
        });
        return "Persisting complete!\n";
    }

    @CliCommand(value = "darklaunch:setusers", help = "Sets the darklaunch switch to true or false for a given userbase")
    public String darkLaunchUsers(
            @CliOption(key = {"redis"}, mandatory = false, help = "The redis node to connect to (defaults to 1)",
                    unspecifiedDefaultValue = "1") final int redis,
            @CliOption(key = {"mysql"}, mandatory = false, help = "The mysql node to connect to (defaults to 1)",
                    unspecifiedDefaultValue = "1") final int mysql,
            @CliOption(key = {"dev"}, mandatory = false, help = "A switch if the database is in dev mode (default = true)",
                    unspecifiedDefaultValue = "true") final String dev,
            @CliOption(key = {"application"}, mandatory = true, help = "The application of which we want to launch a feature darkly",
                    unspecifiedDefaultValue = "*") final String application,
            @CliOption(key = {"name"}, mandatory = true, help = "The name of the feature to darkly launch",
                    unspecifiedDefaultValue = "*") final String name,
            @CliOption(key = {"usernamebase"}, mandatory = false, help = "The names of the users to set the switch on (* wildcards are permitted)",
                    unspecifiedDefaultValue = "*") final String usernamebase,
            @CliOption(key = {"percentage"}, mandatory = false, help = "The percentage of the users (be aware that if usernamebase is set, " +
                    "it is the percentage of that base that will be set)",
                    unspecifiedDefaultValue = "100") final Double percentage,
            @CliOption(key = {"switch"}, mandatory = false, help = "The switch to set (default = true, false will block the user)",
                    unspecifiedDefaultValue = "true") final Boolean latch) throws Exception {
        logger.info("Darklaunching users...\n");

        RedisHost redisHost = getRedisHost(redis);
        MysqlHost mysqlHost = getMysqlHost(mysql);

        Map<RedisTransferArguments, String> redisArgs = ImmutableMap.<RedisTransferArguments, String>builder()
                .put(RedisTransferArguments.HOST, redisHost.getHost())
                .put(RedisTransferArguments.PORT, redisHost.getPort())
                .put(RedisTransferArguments.PASSWORD, redisHost.getPassword()).build();

        Map<MysqlTransferArguments, String> mysqlArgs = ImmutableMap.<MysqlTransferArguments, String>builder()
                .put(MysqlTransferArguments.HOST, mysqlHost.getHost())
                .put(MysqlTransferArguments.PORT, mysqlHost.getPort())
                .put(MysqlTransferArguments.USERNAME, mysqlHost.getUsername())
                .put(MysqlTransferArguments.DATABASE, mysqlHost.getDatabase())
                .put(MysqlTransferArguments.DEV, dev)
                .put(MysqlTransferArguments.PASSWORD, mysqlHost.getPassword()).build();

        AnnotationConfigApplicationContext redisComponentContext = getApplicationContext("redisArgs", redisArgs, RedisConfig.class);
        AnnotationConfigApplicationContext mysqlComponentContext = getApplicationContext("mysqlArgs", mysqlArgs, MysqlConfig.class);

        final RedisTemplate<RedisKey, String> redisKeyTemplate = (RedisTemplate) redisComponentContext.getBean("redisKeyTemplate");
        final JdbcTemplate jdbcTemplate = (JdbcTemplate) mysqlComponentContext.getBean("jdbcTemplate");

        List<String> usernames = jdbcTemplate.query("select username from accounts where username like ?",
                new Object[]{usernamebase.replace('*', '%')},
                new RowMapper<String>() {
                    public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                        return rs.getString(rs.findColumn("username"));
                    }
                });

        final RedisKey darkKey = new RedisKeyBuilder().key(application).type(KeyType.DARK).data(name).build();
        Random random = new Random();
        float chance = random.nextFloat();
        Long count = 0L;
        for (String username : usernames) {
            if (chance <= percentage / 100) {
                if (latch) {
                    redisKeyTemplate.boundHashOps(darkKey).put(username, RedisDarkLauncher.DARK_LAUNCH_ALLOWED_VALUE);
                } else {
                    redisKeyTemplate.boundHashOps(darkKey).put(username, RedisDarkLauncher.DARK_LAUNCH_DISALLOWED_VALUE);
                }
                logger.info("Darkly " + (latch ? "" : "un") + "launching funtionality: " + name + " for user: " + username);
                count++;
            }
        }
        return "Darklaunching complete for [" + count + "] users !\n";
    }

    @CliCommand(value = "darklaunch:info", help = "Darklauchn info of a given userbase")
    public String darkLaunchInfo(
            @CliOption(key = {"redis"}, mandatory = false, help = "The redis node to connect to (defaults to 1)",
                    unspecifiedDefaultValue = "1") final int redis,
            @CliOption(key = {"application"}, mandatory = true, help = "The application of which we want to obtain info about a darkly feature",
                    unspecifiedDefaultValue = "*") final String application,
            @CliOption(key = {"name"}, mandatory = true, help = "The name of the darkly launched feature",
                    unspecifiedDefaultValue = "*") final String name,
            @CliOption(key = {"usernamebase"}, mandatory = false, help = "The names of the users to set the switch on (* wildcards are permitted)",
                    unspecifiedDefaultValue = "*") final String usernamebase) throws Exception {
        logger.info("Darklaunching info...\n");

        RedisHost redisHost = getRedisHost(redis);

        Map<RedisTransferArguments, String> redisArgs = ImmutableMap.<RedisTransferArguments, String>builder()
                .put(RedisTransferArguments.HOST, redisHost.getHost())
                .put(RedisTransferArguments.PORT, redisHost.getPort())
                .put(RedisTransferArguments.PASSWORD, redisHost.getPassword()).build();

        AnnotationConfigApplicationContext redisComponentContext = getApplicationContext("redisArgs", redisArgs, RedisConfig.class);

        final RedisTemplate<RedisKey, String> redisKeyTemplate = (RedisTemplate) redisComponentContext.getBean("redisKeyTemplate");

        final RedisKey darkKey = new RedisKeyBuilder().key(application).type(KeyType.DARK).data(name).build();

        final BoundHashOperations<RedisKey, String, String> hashOperations = redisKeyTemplate.boundHashOps(darkKey);
        if (hashOperations.entries() != null && hashOperations.entries().size() != 0) {
            logger.warning(tabValues(MEDIUM_TAB_PADDING, USER_TITLE_HEADER, "LAUNCHED"));
            for (Map.Entry<String, String> userEntry : hashOperations.entries().entrySet()) {
                if (MatchingUtil.matchesWildcard(RedisKey.REDIS_WILDCARD, usernamebase, userEntry.getKey())) {
                    logger.info(tabValues(MEDIUM_TAB_PADDING, userEntry.getKey(), userEntry.getValue()));
                }
            }
        } else {
            logger.info("No info found for functionality: " + name + " of application: " + application);
        }
        logger.info(DUMMY_SPACE);
        return "Darklaunching info complete!\n";
    }

    private void findAndDo(String argsBeanName, Map<RedisTransferArguments, String> args, String application, String category, KeyType type, String data,
                           RedisFoundCallback redisFoundCallback) {
        try {
            AnnotationConfigApplicationContext componentContext = getApplicationContext(argsBeanName, args, RedisConfig.class);

            final RedisTemplate<RedisKey, String> redisKeyTemplate = (RedisTemplate) componentContext.getBean("redisKeyTemplate");
            final RedisTemplate<String, String> stringRedisTemplate = (RedisTemplate) componentContext.getBean("stringRedisTemplate");

            Set<String> allApplicationKeys = stringRedisTemplate.keys(application + RedisKey.REDIS_WILDCARD);
            Set<String> sortedKeys = new TreeSet<String>(allApplicationKeys);

            Integer deletedKeys = 0;
            for (String applicationKey : sortedKeys) {
                final RedisKey redisKey = RedisKey.deSerialize(applicationKey);
                boolean matchesType = (type == KeyType.ALL | redisKey.getType() == type);
                boolean matchesData = data.equals(RedisKey.REDIS_WILDCARD)
                        || (StringUtils.hasText(redisKey.getData()) && redisKey.getData().toLowerCase().contains(data.toLowerCase()));
                boolean matchesCategory = category.equals(RedisKey.REDIS_WILDCARD)
                        || (StringUtils.hasText(redisKey.getCategory()) && redisKey.getCategory().toLowerCase().contains(category.toLowerCase()));
                if (matchesType && matchesData && matchesCategory) {
                    deletedKeys++;
                    redisFoundCallback.doInRedis(redisKey, redisKeyTemplate);
                }
            }
            logger.info("N° of keys manipulated: " + deletedKeys);
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Fail!", e);
        }
        logger.info(DUMMY_SPACE);
    }

    public RedisHost getRedisHost(final int node) {
        Predicate<RedisHost> matches = new Predicate<RedisHost>() {
            @Override
            public boolean apply(RedisHost redisHost) {
                return redisHost.getNode() == node;
            }
        };
        Iterable<RedisHost> filteredHosts = Iterables.filter(redisHosts, matches);
        return Iterables.getFirst(filteredHosts, null);
    }

    public MysqlHost getMysqlHost(final int db) {
        Predicate<MysqlHost> matches = new Predicate<MysqlHost>() {
            @Override
            public boolean apply(MysqlHost mysqlHost) {
                return mysqlHost.getNode() == db;
            }
        };
        Iterable<MysqlHost> filteredHosts = Iterables.filter(mysqlHosts, matches);
        return Iterables.getFirst(filteredHosts, null);
    }

    private String getTtl(RedisTemplate<RedisKey, String> redisKeyTemplate, final RedisKey redisKey) {
        return redisKeyTemplate.execute(new RedisCallback<String>() {
            @Override
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                Long ttl = connection.ttl(redisKey.serialize().getBytes());
                if (ttl != -1) {
                    DateTime now = new DateTime();
                    DateTime expireDate = now.plus(Period.seconds(safeLongToInt(ttl)));
                    PrettyTime prettyTime = new PrettyTime(now.toDate());
                    return String.valueOf(prettyTime.format(expireDate.toDate()));
                } else {
                    return NEVER_EXPIRATION_MESSAGE;
                }
            }
        });
    }

    private interface RedisFoundCallback {

        void doInRedis(RedisKey redisKey, RedisTemplate<RedisKey, String> redisKeyTemplate);

    }

}
